fixing/merge-conflict - #1
Open
Pavitra-programmers wants to merge 4116 commits into
Open
Conversation
Rebuilds step 2 of the organization install guide around the fresh-install case, matching the approved Paper design: - Eyebrow, "Open the file you just downloaded" heading, a file chip naming the real installer artifact, two numbered actions, a calm amber OS trust note, a handoff note, an OR divider, the demoted already-installed row with the single dark primary button, and a quiet manual-paste disclosure. - Copy is per-OS (macOS / Windows / Linux) and follows the platform the user actually downloaded, falling back to browser platform detection. - The file chip shows the real published artifact name from genericInstallerArtifactName() (no version, because the published assets carry none) and is omitted when the platform or arch is unknown, so the page never invents a filename. - The handoff note describes what the installer really does instead of promising that this page auto-advances to step 3, which it cannot observe: the installer resolves its own activation grant from the install token, so the web page's code is a different code. - Installer preview: fake CTA flattened to a hairline outline, badge relabelled "PREVIEW · WHAT YOU'LL SEE ON YOUR COMPUTER" with a neutral dot, and the column stretches to match the left column height. Eval flows are updated for the new copy and for the manual-paste disclosure. Co-authored-by: Cursor <cursoragent@cursor.com>
…#3107) The expired-install-link assertion pinned copy that no longer shipped, so the suite failed on a clean checkout. Assert the actual contract (no raw API error code leaks, the message names the link, expiry, and who to ask) so wording can improve without breaking the test.
The installer artifact shipped in v0.18.1 reported 1.0.0 because the mac Info.plist hardcoded it and nothing else carried a version at all, so a user or support engineer could not tell which build they had. - resolve one version from the release tag (local builds fall back to apps/installer/package.json) and stamp it into the compiled binary, the mac Info.plist, the DMG volume name, and the Windows version resource - add `--version` to the installer CLI and a quiet footer in the window - keep apps/installer/package.json on the release train via bump-version Published asset filenames are unchanged.
This reverts commit 9a40556.
Step 2 told macOS users to drag the app into Applications. The disk image contains a single item, Install OpenWork, and no Applications shortcut, so there is nothing to drag: you double-click that installer and choose Install. Also stop attributing the installer's work to the app. The downloaded file is the installer, it is what the OS warns about, and it is what installs the app and opens the browser to approve this computer. Co-authored-by: Cursor <cursoragent@cursor.com>
…#3120) The flow still asserted the pre-DMG world: zip assets named openwork-installer-mac-arm64.zip, an "OpenWork Installer.app" inside them, a publish-generic-installer job, and a HEAD probe in the artifact resolver. None of those exist now, so four assertions could only fail and the download path could not validate a real artifact at all. Validate the published disk images instead: check the UDIF trailer everywhere, and on macOS verify, mount, and assert "Install OpenWork.app" at the image root with codesign, Gatekeeper and notarization. Replace the closed v0.17.19 recovery step with the releases/latest URL den-api actually redirects to, and point the pipeline assertions at the reusable workflow that owns the asset names plus the download route that resolves them. The version stamp is only asserted for the prerelease built from the pull request, because releases up to and including v0.18.1 predate that fix. Co-authored-by: Cursor <cursoragent@cursor.com>
…t path (#3122) An external MCP capability got 30s for the whole session -- connect, OAuth refresh, initialize and tools/call shared one budget -- and execute_capability spent that budget twice, once for schema-digest discovery and once for the call. Slow providers died at ~31s. The 120s/150s tool budget from #2750 was never reached. #2810 deleted the legacy runtime, so callExternalMcpTool resolves to the enterprise client, and the adapter's tool-call path forwarded no lifecycle deadline -- unlike listExternalMcpTools beside it -- leaving the package's 30s default in charge. A Math.min(operationTimeoutMs, ...) clamp meant injecting the longer deadline would have been clipped back to 30s anyway. The only test asserting 120s exercised the bypassed file. Make an injected lifecycle deadline authoritative in both directions and demote operationTimeoutMs to the fallback default. Forward the deadline on the tool-call path, and share one deadline between discovery and the call so the handshake and the budget are paid once. Split the per-request timeout from the absolute bound and stop cancelling a provider that is still reporting progress, matching the calling harness instead of being stricter than it; the SDK only attaches a progressToken when an onprogress handler is present, so the flag alone was inert.
Every dev Electron instance shared one profile directory, so app.requestSingleInstanceLock() failed for the second worktree. Chromium had already opened the CDP port by then, so the process lingered answering /json/version without ever creating a window or logging anything - it looked exactly like a hang. Add OPENWORK_DEV_PROFILE (dev-only, inert when packaged): unset keeps today's shared profile so existing profiles are untouched, 'auto' derives a stable name from the worktree path, and any other value names a profile explicitly. OPENWORK_ELECTRON_USERDATA still wins. On lock contention the second dev instance now explains which profile directory is held and how to isolate, then exits for real so the CDP port is released. Dev startup prints the resolved profile directory and CDP address. Add pnpm dev:worktree for the common case: auto profile plus free CDP and Vite ports, so several worktrees run side by side.
…fetch (#3127) * fix(desktop): route updater manifest through OS trust and guard bare fetch The architecture-mismatch banner fetched the electron-updater manifest from github.com with bare Node fetch, so it only trusted bundled Mozilla roots and ignored system proxy configuration. On a TLS-inspecting corporate network that single call failed while the rest of the app worked, with no timeout to bound it. Route it through electronNet.fetch (Chromium: OS trust store + system proxy) with a 10s abort, keeping the warn-and-return-null failure behavior. apps/server/src already bans bare fetch through a test. Extend the same enforcement to the two remaining surfaces that talk to the network: - apps/desktop/electron: unmarked bare fetch fails; the four genuine loopback call sites (sidecar health, runtime JSON, daemon shutdown, CDP discovery) carry a // loopback-fetch: marker naming why 127.0.0.1 is provable. - apps/installer/src: bare fetch fails; external requests must use fetchWithSystemCa. system-ca.ts (defines the wrapper) and ui-html.ts (browser-side template) are exempt; Bun.serve's `async fetch(request)` handler is recognized as a definition, not a call. No configuration, API, or chart surface changes. * fix(desktop): clarify electron fetch trust guard
…#3131) * docs(enterprise): document outbound network access and guard it in CI Enterprise networks block outbound connections by default and IT must approve each destination, but nothing in this repo listed what OpenWork needs to reach. Two destinations were named nowhere at all: - objects.githubusercontent.com, the redirect target of every release-asset download. Approving only github.com lets a download start and then fail partway through. - registry.npmjs.org, used by `npx -y openwork-ui-mcp` in packaged builds. Blocked, the UI-control MCP is silently unavailable while the app still opens. Add docs/enterprise/outbound-access.md for customer IT, in plain language, with a copy-pasteable minimum allowlist and a what-breaks-when-blocked column. Desktop client access is documented separately from self-hosted Den access because different teams approve them on different networks. Back it with docs/enterprise/outbound-access.json and scripts/check-outbound-access.mjs, wired into ci-tests.yml, so a feature that introduces a new destination must document its enterprise impact. The check also reports manifest entries that no longer exist in code, so the list cannot rot. Hosts reached indirectly (redirect targets, subprocesses, links, schema strings) are classified by kind and exempt from the staleness rule. Docs, one check script, one CI step, one package script. No product behavior, configuration, API, or chart surface changes. * docs(enterprise): name the current GitHub release-asset host Verified against a live OpenWork release asset: github.com now redirects release downloads to release-assets.githubusercontent.com, not objects.githubusercontent.com. An allowlist naming only the old host would still let downloads start and then fail partway through, which is the exact symptom this page exists to prevent. List both. release-assets.githubusercontent.com is what GitHub serves today; objects.githubusercontent.com stays documented for older clients and any staged rollback.
Den's SSRF guard refuses to fetch private/reserved addresses on behalf of users, which is correct on hosted multi-tenant deployments. But a common enterprise topology puts Den inside the customer's private network with their MCP servers on private addresses, and there the guard blocks every internal server. The operator sees MCP_URL_BLOCKED and is told to "use a public HTTPS MCP URL or change the deployment's private-network policy through security review", with no indication that a supported opt-out exists. env.ts has provided DEN_ALLOW_PRIVATE_MCP_URLS=1 all along, but it appeared only in source, unit tests, and one eval flow: not in .env.example, not in the chart, not in any doc. Surface it in all three places so the fix is reachable without reading the source. Scoped to den-api only. den-api, den-web, and inference all envFrom the same ConfigMap, so adding the key there would inject an SSRF-bypass flag into two workloads that never read it; the value is emitted directly on the den-api container instead. Only den-api consumes allowPrivateMcpUrls (mcp-connections, plugin-system/store, enterprise-mcp-client-adapter, external-mcp-client). Nothing renders unless an operator opts in, so helm template with default values is byte-identical to the previous chart: no ConfigMap checksum change and no pod restart on upgrade. Blank, 0, and false render nothing; only "1" enables it, matching the trimmed string comparison in env.ts; any other value fails the render rather than guessing. url-guard.ts and env.ts are untouched.
…#3132) Settings > Debug runs a bounded initialize -> tools/list check against the managed openwork-cloud MCP entry. It carries an organization MCP bearer token, so the probe refuses to send that credential to an origin outside its trust list. DEFAULT_TRUSTED_ORIGINS holds only the hosted origins, so on every self-hosted deployment the check returns untrusted_endpoint and never runs. The escape hatch, OPENWORK_AGENT_DIAGNOSTICS_TRUSTED_ORIGINS, appeared in exactly two places in the repo: its own source file and its own test. The guidance an operator actually saw was "have the server administrator explicitly trust the development origin" -- which calls a production on-prem Den a development origin and never names the variable, so nobody could act on it. Name the variable in the operator action, and document it in the self-hosting guide with the real format rules enforced by configuredTrustedOrigins: bare origins, comma-separated, https except loopback, invalid entries ignored rather than failing open, additive to the hosted defaults. The docs state explicitly that this enables the diagnostic only and that Cloud MCP itself works without it, so operators do not read a skipped probe as a broken deployment. The trust model is deliberately unchanged: agent-context-cloud-probe.ts is untouched, so which origins are trusted cannot differ from before. Trusting the endpoint's own origin would be circular, since the probe exists to validate the very config that names it. Tests pin that an unlisted self-hosted origin still performs no fetch, and that the action keeps naming the variable.
…yment (#3135) Most enterprise installs are not fully air-gapped. The common shape is laptops with internet plus VPN, and Den web and Den API inside the customer's private network. That split was undocumented, so every customer rediscovered its gotchas by hitting failures. Add a published page covering the two network paths separately, because different teams approve them: what the desktop must reach, what Den must reach (only MySQL and ghcr.io at pull time are hard requirements), and the three things that surprise operators, each with symptom, cause, and fix: - Internal MCP servers are refused by the SSRF guard until DEN_ALLOW_PRIVATE_MCP_URLS=1, with the security tradeoff stated. - The Settings > Debug cloud catalog probe is skipped because only hosted origins are trusted. It states explicitly that this affects the diagnostic only and Cloud MCP still works, so a skipped probe is not read as an outage, and that the variable must be set before launch because the in-app environment store strips OPENWORK_* keys. - Den's outbound self-check targets a public host by default and can be self-hosted with DEN_DIAGNOSTICS_ORIGIN. Also resolve a contradiction that decided whether IT opens one hostname or two. docs/org-install-links.md claimed "the desktop must be able to reach both origins"; resolveDenBaseUrls ignores an explicit API origin when a base URL is present and derives <baseUrl>/api/den, proven by the existing den-mcp-url test. Steady-state desktop traffic needs one origin. A separate API origin is only reachable-critical for the one-time install-link exchange, which builds its endpoint from the apiBaseUrl carried in the link, and for external MCP clients. Both docs now state their context instead of disagreeing.
Most enterprise installs are not fully air-gapped: laptops have internet plus VPN while Den web and Den API sit inside the customer's private network. That shape depends on four guarantees that no test held, so a refactor could change on-prem behavior, or an enterprise firewall requirement, without anything turning red. Add an internal flow with four frames, each backed by real evidence rather than prose: 1. resolveDenBaseUrls ignores an explicitly configured API origin when a base URL is present, so the desktop keeps needing exactly one Den hostname. 2. url-guard refuses private and reserved MCP addresses by default, and DEN_ALLOW_PRIVATE_MCP_URLS is the only documented way in. 3. The cloud catalog probe returns untrusted_endpoint and performs no request for an untrusted origin, so a self-hosted deployment leaks no organization MCP token and the diagnostic is skipped rather than failed. 4. DEN_DIAGNOSTICS_ORIGIN selects the outbound self-check target, with the public host as an explicit default, and the bearer token minimum of 24 characters is enforced. Runs with no app, no database, and no network: requiresApp is false and it passes against a dead CDP URL. Evidence comes from focused Bun test filters plus direct imports of the pure functions. Verified it can fail: breaking resolveDenBaseUrls fails frame 1 and forcing allowPrivateMcpUrls true fails frame 2, each naming the broken property.
…ce (#3139) * docs(enterprise): document outbound network access and guard it in CI Enterprise networks block outbound connections by default and IT must approve each destination, but nothing in this repo listed what OpenWork needs to reach. Two destinations were named nowhere at all: - objects.githubusercontent.com, the redirect target of every release-asset download. Approving only github.com lets a download start and then fail partway through. - registry.npmjs.org, used by `npx -y openwork-ui-mcp` in packaged builds. Blocked, the UI-control MCP is silently unavailable while the app still opens. Add docs/enterprise/outbound-access.md for customer IT, in plain language, with a copy-pasteable minimum allowlist and a what-breaks-when-blocked column. Desktop client access is documented separately from self-hosted Den access because different teams approve them on different networks. Back it with docs/enterprise/outbound-access.json and scripts/check-outbound-access.mjs, wired into ci-tests.yml, so a feature that introduces a new destination must document its enterprise impact. The check also reports manifest entries that no longer exist in code, so the list cannot rot. Hosts reached indirectly (redirect targets, subprocesses, links, schema strings) are classified by kind and exempt from the staleness rule. Docs, one check script, one CI step, one package script. No product behavior, configuration, API, or chart surface changes. * docs(self-host): make the cloud catalog diagnostic actionable on-prem Settings > Debug runs a bounded initialize -> tools/list check against the managed openwork-cloud MCP entry. It carries an organization MCP bearer token, so the probe refuses to send that credential to an origin outside its trust list. DEFAULT_TRUSTED_ORIGINS holds only the hosted origins, so on every self-hosted deployment the check returns untrusted_endpoint and never runs. The escape hatch, OPENWORK_AGENT_DIAGNOSTICS_TRUSTED_ORIGINS, appeared in exactly two places in the repo: its own source file and its own test. The guidance an operator actually saw was "have the server administrator explicitly trust the development origin" -- which calls a production on-prem Den a development origin and never names the variable, so nobody could act on it. Name the variable in the operator action, and document it in the self-hosting guide with the real format rules enforced by configuredTrustedOrigins: bare origins, comma-separated, https except loopback, invalid entries ignored rather than failing open, additive to the hosted defaults. The docs state explicitly that this enables the diagnostic only and that Cloud MCP itself works without it, so operators do not read a skipped probe as a broken deployment. The trust model is deliberately unchanged: agent-context-cloud-probe.ts is untouched, so which origins are trusted cannot differ from before. Trusting the endpoint's own origin would be circular, since the probe exists to validate the very config that names it. Tests pin that an unlisted self-hosted origin still performs no fetch, and that the action keeps naming the variable. * docs(helm): expose the private-network MCP opt-out to operators Den's SSRF guard refuses to fetch private/reserved addresses on behalf of users, which is correct on hosted multi-tenant deployments. But a common enterprise topology puts Den inside the customer's private network with their MCP servers on private addresses, and there the guard blocks every internal server. The operator sees MCP_URL_BLOCKED and is told to "use a public HTTPS MCP URL or change the deployment's private-network policy through security review", with no indication that a supported opt-out exists. env.ts has provided DEN_ALLOW_PRIVATE_MCP_URLS=1 all along, but it appeared only in source, unit tests, and one eval flow: not in .env.example, not in the chart, not in any doc. Surface it in all three places so the fix is reachable without reading the source. Scoped to den-api only. den-api, den-web, and inference all envFrom the same ConfigMap, so adding the key there would inject an SSRF-bypass flag into two workloads that never read it; the value is emitted directly on the den-api container instead. Only den-api consumes allowPrivateMcpUrls (mcp-connections, plugin-system/store, enterprise-mcp-client-adapter, external-mcp-client). Nothing renders unless an operator opts in, so helm template with default values is byte-identical to the previous chart: no ConfigMap checksum change and no pod restart on upgrade. Blank, 0, and false render nothing; only "1" enables it, matching the trimmed string comparison in env.ts; any other value fails the render rather than guessing. url-guard.ts and env.ts are untouched. * docs(enterprise): name the current GitHub release-asset host Verified against a live OpenWork release asset: github.com now redirects release downloads to release-assets.githubusercontent.com, not objects.githubusercontent.com. An allowlist naming only the old host would still let downloads start and then fail partway through, which is the exact symptom this page exists to prevent. List both. release-assets.githubusercontent.com is what GitHub serves today; objects.githubusercontent.com stays documented for older clients and any staged rollback. * docs(self-host): document the private-network (semi air-gapped) deployment Most enterprise installs are not fully air-gapped. The common shape is laptops with internet plus VPN, and Den web and Den API inside the customer's private network. That split was undocumented, so every customer rediscovered its gotchas by hitting failures. Add a published page covering the two network paths separately, because different teams approve them: what the desktop must reach, what Den must reach (only MySQL and ghcr.io at pull time are hard requirements), and the three things that surprise operators, each with symptom, cause, and fix: - Internal MCP servers are refused by the SSRF guard until DEN_ALLOW_PRIVATE_MCP_URLS=1, with the security tradeoff stated. - The Settings > Debug cloud catalog probe is skipped because only hosted origins are trusted. It states explicitly that this affects the diagnostic only and Cloud MCP still works, so a skipped probe is not read as an outage, and that the variable must be set before launch because the in-app environment store strips OPENWORK_* keys. - Den's outbound self-check targets a public host by default and can be self-hosted with DEN_DIAGNOSTICS_ORIGIN. Also resolve a contradiction that decided whether IT opens one hostname or two. docs/org-install-links.md claimed "the desktop must be able to reach both origins"; resolveDenBaseUrls ignores an explicit API origin when a base URL is present and derives <baseUrl>/api/den, proven by the existing den-mcp-url test. Steady-state desktop traffic needs one origin. A separate API origin is only reachable-critical for the one-time install-link exchange, which builds its endpoint from the apiBaseUrl carried in the link, and for external MCP clients. Both docs now state their context instead of disagreeing. * docs(self-host): consolidate network, air-gap, and certificate guidance Promote Self-host from a small Start here group to a dedicated docs tab while preserving every existing start-here URL. Consolidate scattered network material into five focused published pages: air-gapped deployment, outbound network access, certificate trust and proxies, installer delivery, and network diagnostics. Keep the private-network topology page as the high-level entry. Make boundaries explicit instead of conflating them: - mounted installers solve installer delivery, not full product isolation; - desktop Chromium, spawned sidecars, Den containers, and MySQL use different trust surfaces; - Linux Electron may require NSS/user trust even when /etc/ssl is correct; - sslmode=require encrypts but is not certificate identity verification; - a skipped Cloud catalog diagnostic does not mean Cloud MCP is broken. Promote the machine-guarded outbound manifest into customer-facing docs, retain repository operator pages and their existing anchors, and slim the self-host/private-network overviews so there is one canonical owner per detail. The Helm and diagnostics READMEs remain authoritative for low-level mechanics. This branch is stacked on PRs #3131, #3132, #3133, and #3135. It changes docs and navigation only; no product, Helm template, environment, skill, or eval behavior changes.
* test(den): add complete skill CRUD proof flow * fix(evals): support native MySQL in Daytona
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.21.0 to 4.23.1. - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](privatenumber/tsx@v4.21.0...v4.23.1) --- updated-dependencies: - dependency-name: tsx dependency-version: 4.23.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@codemirror/lang-markdown](https://github.com/codemirror/lang-markdown) from 6.5.0 to 6.5.1. - [Changelog](https://github.com/codemirror/lang-markdown/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/lang-markdown/commits) --- updated-dependencies: - dependency-name: "@codemirror/lang-markdown" dependency-version: 6.5.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* build(deps): bump next from 16.2.1 to 16.2.11 Bumps [next](https://github.com/vercel/next.js) from 16.2.1 to 16.2.11. - [Release notes](https://github.com/vercel/next.js/releases) - [Commits](vercel/next.js@v16.2.1...v16.2.11) --- updated-dependencies: - dependency-name: next dependency-version: 16.2.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * fix: pin next browser mapping transitive --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: OmarMcAdam <gh@mcadam.io>
* build(deps): bump react-dom and @types/react-dom Bumps [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together. Updates `react-dom` from 18.2.0 to 19.2.8 - [Release notes](https://github.com/react/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom) Updates `@types/react-dom` from 18.2.25 to 19.2.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) --- updated-dependencies: - dependency-name: "@types/react-dom" dependency-version: 19.2.3 dependency-type: direct:development update-type: version-update:semver-major - dependency-name: react-dom dependency-version: 19.2.8 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: align react catalog with react dom --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: OmarMcAdam <gh@mcadam.io>
…3376) The boot takeover showed a progress bar hardcoded to two thirds that only pulsed, so a slow sandbox read as frozen at 66%. It also ran two spinners at once, and Retry appeared only after a hard failure, leaving a slow but healthy boot with nothing to do. Progress is now derived from the status we already poll, so it can never claim more than we can prove: a provisioning sandbox is still being reserved, while a waking or updating one demonstrably exists. Only the active checkpoint animates. At 45s the copy escalates and Retry / Sign out appear. Also fixes the dot ticker, which computed to fully transparent: the --dls-*-rgb tokens are space-separated but the rules used legacy comma rgba(), so the indicator was invisible everywhere it was used, including the composer wait state. Co-authored-by: Cursor <cursoragent@cursor.com>
…3375) Providers were identifiable only by a two-letter monogram unless they were one of the three brands with a bundled inline SVG, which made the connect dialog read as a wall of grey initials. Adds a logo resolution ladder (inline SVG, Simple Icons, favicon, monogram last) so a provider only falls back to initials once every source is exhausted. Long-tail catalog ids are slugified domains, so unslugging "302ai" to "302.ai" recovers a real mark instead of "30". Builds on that to group the dialog into Connected and All providers, lead the API-key step with the provider's own logo, and make the model picker's API-keys row reflect whether any keys exist yet. Co-authored-by: Cursor <cursoragent@cursor.com>
better-sqlite3 11.10.0 ships no `node-v137` (Node 24) prebuild, so every CI
install fell through `prebuild-install` to `node-gyp rebuild`, which downloads
the Node headers from nodejs.org mid-install. When that request stalls the whole
install dies:
prebuild-install warn install No prebuilt binaries found (target=24.13.0 ...)
gyp ERR! stack AggregateError [ETIMEDOUT]
[ELIFECYCLE] Command failed with exit code 1
Bump better-sqlite3 to ^12.11.1, which publishes node-v137 (and v127/v141/v147)
prebuilds, so installs fetch a binary instead of compiling. 12.0.0 was a
build-matrix-only major (dropped EOL Node 18 / Electron 26-28); the JS API is
unchanged and Electron 35 (ABI 133) prebuilds are still published.
Also cache the pnpm store in the tests workflow, matching the release/alpha
workflows: OpenWork Tests was re-downloading all 1453 packages every run
("reused 0"), which is both slow and extra network surface to flake on.
…ehind its components, timeline observability (#3359) * feat(evals): timeline primitive, workspace seeding, and honest visual expectations * fix(evals): seed completes onboarding and engine boot; wait on product state not route shapes * fix(evals): keep app-smoke on the product's own workspace creation path * test(evals): app pointed at a TLS-broken den must name the fault (welcome surface, no onboarding needed) * chore(evals): single-spec sandbox runner * test(evals): adapt the TLS-fault spec to the real pre-workspace surface * test(evals): choose the sign-in control from real buttons * test(evals): TLS-intercepted den — no false 'connected', and diagnostics name the interception * test(evals): one frame — sign-in against a broken den changes nothing visually * test(evals): name the TLS spec after what it actually proves * fix(evals): short per-probe timeouts so busy renderers are retried, not waited on * fix(evals): read the created workspace via the proven in-page server call, not an unverified bridge channel * test(evals): poll patiently through the cold-profile renderer block * fix(evals): poll for the created workspace record through renderer blocks * fix(evals): resolve the workspace id from product state after onboarding instead of a hanging server fetch * fix(evals): idempotent reads retry through renderer blocks * fix(evals): retry reads to a deadline, not a fixed attempt count * fix(evals): re-attach to the app's current page target when evaluations stop answering * fix(evals): background prep does not make the welcome surface un-interactive * chore(evals): verify the X display actually answers before spawning Electron * chore(evals): clear stale Electron processes before each sandbox spec run * fix(evals): resolve the workspace id after onboarding completes, not before * fix(evals): report the workspace id even while the welcome surface is showing * test(evals): assert the first-run workspace id after onboarding adopts it * test(evals): use the resolved workspace id; report loaded skills on failure * fix(evals): parse the skill name from the row, and drop a regex that never compiled * test(evals): wait for the assistant to settle; use the session control when the button is not text * test(evals): wait for output to stabilise; stop assuming session ids appear in the route * fix(evals): settings and extensions surfaces are interactive too * test(evals): read the created session id from the product's session list * test(evals): drop an exact timing threshold that can flake by a millisecond * test(evals): first run signs in through a real browser, then shares a skill via a marketplace * chore(evals): verify the display in the full runner; give single-spec runs den env * refactor(evals): move environment healing behind the components that own it Target re-attach now lives in @openwork/cdp (evaluateOnSurface), display liveness and stale-surface cleanup in @openwork/hosts (spawnElectron), and resolveHost no longer picks the Daytona host when the caller is already inside a sandbox. The sandbox runner scripts drop the logic they were carrying. * test(evals): match the real desktop-auth handoff URL and read Den's deep link from the browser * test(evals): switch the browser from sign-up to sign-in for a seeded account * chore(evals): let a single-spec run ensure the Den stack when the spec needs it * test(evals): collapse workspace arrangement to the proven onboarding path seedWorkspace arranged a workspace over the local server API and left the engine unconfigured and the model catalog empty — a state the product's own onboarding never produces, so specs failed on arrangement instead of their subject. Every spec now goes through createAndSelectWorkspace, the path the passing journeys already proved. Also read the created session id from the field session.list_sessions really returns: items carry sessionId, not id (probed live; the old extraction never matched, so the wait timed out even with sessions present). * test(evals): drive den web's real two-step sign-in and read the sign-in code input Probed live on a running den: auth is an email-only step (Next) that routes to a password step (Sign in) — there is no single form and no sign-up/sign-in toggle. After sign-in the deep link is not an anchor or page text; it is a readonly input holding the openwork://den-auth?grant=… sign-in code next to Open OpenWork, so read it from there. * fix(evals): keep pnpm version redirection warm inside isolated Electron profiles Each surface's fresh HOME hides the host's pnpm tool cache, so the pinned pnpm (packageManager) is re-downloaded from the network on every spawn (21MB). On the eval sandbox one failed download degenerated into a self-sustaining recursive 'pnpm add pnpm' cascade that outlived the run and starved later spawns. Point PNPM_HOME at the host's real pnpm home; the redirect is then local and instant (verified: fresh HOME resolves in 0.75s with no downloads). * chore(evals): export den eval env only when a den actually answers Exporting OPENWORK_EVAL_DEN_API_URL unconditionally made den-gated specs run against nothing and die in 3ms with 'fetch failed' instead of skipping with their own reason. Probe /health first; with OPENWORK_SPEC_NEEDS_DEN=1 the stack is still ensured before the probe. * test(evals): scroll the claimed skill row into view before its screenshot The skills menu is a scrollable list: /browser-automation was loaded (the DOM assertions passed) while the screenshot showed only the rows above it, so the visual validator honestly reported it absent. * fix(evals): survive renderer freeze bursts while reading the skills menu The workspace engine boot blocks the renderer JS thread in bursts. Bare 20s evaluations (plug-menu clicks) died with raw CDP timeouts, and the measure evaluations gave their outer CDP call the same 20s deadline as the in-page poll, so the two raced under load. Fold the menu clicks into a guarded, retried wait and give the measures headroom over their inner deadlines. * test(evals): reveal the menu row a visual claim names before its screenshot The capability menu is a scrollable popover: all four sections were in the DOM (the code assertion passed) while Agents sat above the fold, so the validator honestly reported it invisible. Scroll the named row into view and let the code assertion carry section completeness. * test(evals): assert the capability menu visuals a person can actually see The vision loop caught a real product defect: with a long command list the capability popover extends under the window header, so its first section row (Agents) is covered and unclickable (repo workspace, popover top at y=17 under the title bar). Keep four-section completeness as the DOM assertion, scope the visual claim to the visible sections, and record the defect where the claim is made. * test(evals): arm the slow-cloud scenario separately and drive the menu with retried waits The scenario opened the plug menu inside its own evaluate with fixed 100/300ms delays right after a reload; under load those clicks land before React handlers attach and the whole block times out. Arm the fetch delay and the connect witness in one small mutation, open the menu with the guarded retried helper, then measure. * test(evals): arrange the real cloud-connected desktop state for slow-cloud skills The scenario set window.__OPENWORK_GATEWAY__, which flips the app into the hosted gateway runtime and rewires the local openwork-server client to the page origin — so client.listSkills hit the Vite dev server and local skills never rendered, a state no desktop user can reach. The den auth token and active org id it also set are the real cloud-connected desktop arrangement, so keep only those. Drop the unused resetSkillsCloudState. * test(evals): drop the slow-cloud skills block whose arrangement cannot be faithful On desktop, den traffic goes through the main process, so the renderer fetch hook never delayed anything (denRequestCount stayed 0), and the gateway marker it used instead rewired the local server to the page origin. The block therefore only ever measured an impossible state. Cloud latency belongs in a den-boundary fault spec like app-den-tls-fault, where the fault is injected for real.
#3387) * feat(evals): timeline primitive, workspace seeding, and honest visual expectations * fix(evals): seed completes onboarding and engine boot; wait on product state not route shapes * fix(evals): keep app-smoke on the product's own workspace creation path * test(evals): app pointed at a TLS-broken den must name the fault (welcome surface, no onboarding needed) * chore(evals): single-spec sandbox runner * test(evals): adapt the TLS-fault spec to the real pre-workspace surface * test(evals): choose the sign-in control from real buttons * test(evals): TLS-intercepted den — no false 'connected', and diagnostics name the interception * test(evals): one frame — sign-in against a broken den changes nothing visually * test(evals): name the TLS spec after what it actually proves * fix(evals): short per-probe timeouts so busy renderers are retried, not waited on * fix(evals): read the created workspace via the proven in-page server call, not an unverified bridge channel * test(evals): poll patiently through the cold-profile renderer block * fix(evals): poll for the created workspace record through renderer blocks * fix(evals): resolve the workspace id from product state after onboarding instead of a hanging server fetch * fix(evals): idempotent reads retry through renderer blocks * fix(evals): retry reads to a deadline, not a fixed attempt count * fix(evals): re-attach to the app's current page target when evaluations stop answering * fix(evals): background prep does not make the welcome surface un-interactive * chore(evals): verify the X display actually answers before spawning Electron * chore(evals): clear stale Electron processes before each sandbox spec run * fix(evals): resolve the workspace id after onboarding completes, not before * fix(evals): report the workspace id even while the welcome surface is showing * test(evals): assert the first-run workspace id after onboarding adopts it * test(evals): use the resolved workspace id; report loaded skills on failure * fix(evals): parse the skill name from the row, and drop a regex that never compiled * test(evals): wait for the assistant to settle; use the session control when the button is not text * test(evals): wait for output to stabilise; stop assuming session ids appear in the route * fix(evals): settings and extensions surfaces are interactive too * test(evals): read the created session id from the product's session list * test(evals): drop an exact timing threshold that can flake by a millisecond * test(evals): first run signs in through a real browser, then shares a skill via a marketplace * chore(evals): verify the display in the full runner; give single-spec runs den env * refactor(evals): move environment healing behind the components that own it Target re-attach now lives in @openwork/cdp (evaluateOnSurface), display liveness and stale-surface cleanup in @openwork/hosts (spawnElectron), and resolveHost no longer picks the Daytona host when the caller is already inside a sandbox. The sandbox runner scripts drop the logic they were carrying. * test(evals): match the real desktop-auth handoff URL and read Den's deep link from the browser * test(evals): switch the browser from sign-up to sign-in for a seeded account * chore(evals): let a single-spec run ensure the Den stack when the spec needs it * test(evals): collapse workspace arrangement to the proven onboarding path seedWorkspace arranged a workspace over the local server API and left the engine unconfigured and the model catalog empty — a state the product's own onboarding never produces, so specs failed on arrangement instead of their subject. Every spec now goes through createAndSelectWorkspace, the path the passing journeys already proved. Also read the created session id from the field session.list_sessions really returns: items carry sessionId, not id (probed live; the old extraction never matched, so the wait timed out even with sessions present). * test(evals): drive den web's real two-step sign-in and read the sign-in code input Probed live on a running den: auth is an email-only step (Next) that routes to a password step (Sign in) — there is no single form and no sign-up/sign-in toggle. After sign-in the deep link is not an anchor or page text; it is a readonly input holding the openwork://den-auth?grant=… sign-in code next to Open OpenWork, so read it from there. * fix(evals): keep pnpm version redirection warm inside isolated Electron profiles Each surface's fresh HOME hides the host's pnpm tool cache, so the pinned pnpm (packageManager) is re-downloaded from the network on every spawn (21MB). On the eval sandbox one failed download degenerated into a self-sustaining recursive 'pnpm add pnpm' cascade that outlived the run and starved later spawns. Point PNPM_HOME at the host's real pnpm home; the redirect is then local and instant (verified: fresh HOME resolves in 0.75s with no downloads). * chore(evals): export den eval env only when a den actually answers Exporting OPENWORK_EVAL_DEN_API_URL unconditionally made den-gated specs run against nothing and die in 3ms with 'fetch failed' instead of skipping with their own reason. Probe /health first; with OPENWORK_SPEC_NEEDS_DEN=1 the stack is still ensured before the probe. * test(evals): scroll the claimed skill row into view before its screenshot The skills menu is a scrollable list: /browser-automation was loaded (the DOM assertions passed) while the screenshot showed only the rows above it, so the visual validator honestly reported it absent. * fix(evals): survive renderer freeze bursts while reading the skills menu The workspace engine boot blocks the renderer JS thread in bursts. Bare 20s evaluations (plug-menu clicks) died with raw CDP timeouts, and the measure evaluations gave their outer CDP call the same 20s deadline as the in-page poll, so the two raced under load. Fold the menu clicks into a guarded, retried wait and give the measures headroom over their inner deadlines. * test(evals): reveal the menu row a visual claim names before its screenshot The capability menu is a scrollable popover: all four sections were in the DOM (the code assertion passed) while Agents sat above the fold, so the validator honestly reported it invisible. Scroll the named row into view and let the code assertion carry section completeness. * test(evals): assert the capability menu visuals a person can actually see The vision loop caught a real product defect: with a long command list the capability popover extends under the window header, so its first section row (Agents) is covered and unclickable (repo workspace, popover top at y=17 under the title bar). Keep four-section completeness as the DOM assertion, scope the visual claim to the visible sections, and record the defect where the claim is made. * test(evals): arm the slow-cloud scenario separately and drive the menu with retried waits The scenario opened the plug menu inside its own evaluate with fixed 100/300ms delays right after a reload; under load those clicks land before React handlers attach and the whole block times out. Arm the fetch delay and the connect witness in one small mutation, open the menu with the guarded retried helper, then measure. * test(evals): arrange the real cloud-connected desktop state for slow-cloud skills The scenario set window.__OPENWORK_GATEWAY__, which flips the app into the hosted gateway runtime and rewires the local openwork-server client to the page origin — so client.listSkills hit the Vite dev server and local skills never rendered, a state no desktop user can reach. The den auth token and active org id it also set are the real cloud-connected desktop arrangement, so keep only those. Drop the unused resetSkillsCloudState. * test(evals): drop the slow-cloud skills block whose arrangement cannot be faithful On desktop, den traffic goes through the main process, so the renderer fetch hook never delayed anything (denRequestCount stayed 0), and the gateway marker it used instead rewired the local server to the page origin. The block therefore only ever measured an impossible state. Cloud latency belongs in a den-boundary fault spec like app-den-tls-fault, where the fault is injected for real. * test(evals): wait for the sign-in outcome, and give org specs a workspace before sign-in signInInBrowser returned right after clicking Sign in, so the next frame honestly showed 'Working...'. Wait for the signed-in outcome (or the sign-in code) before returning. The signed-in org shell offers no Add workspace entry, and the workspace.create control there reports ok while creating nothing (its error lands in closed-modal state) — probed live twice. Arrange org specs the way a member actually gets there: create the workspace on the proven welcome path first, then sign in; organization onboarding is completed when it appears and the existing workspace is reselected either way. * test(evals): send the role the marketplace access grant API requires POST /v1/marketplaces/:id/access rejects a grant without role (viewer|editor|manager). Viewer is what sharing with a colleague means. * test(evals): read shared skill names from the plugin's resolved components The resolved marketplace payload carries plugins with component counts only; skill names live on GET /v1/plugins/:id/resolved as items[].configObject (objectType skill, title). Read them as the member so visibility stays the colleague's, not the admin's. * test(evals): poll the model catalog past its first paint, and pin the managed-recovery defect The picker can paint 'No models' before the engine's catalog lands (observed live: 0 models on first read, 7 shortly after on the same boot), so the primary test polls up to 90s before asserting. The managed test now pins today's truth: after an admin publishes a provider, Retry does not deliver it to the composer even though the API already entitles the member (probed live, 201 + readable as the member, empty notice survives 90s+). The spec asserts the notice stays honest and the composer stays usable; when live recovery ships, the pinned wait fails loudly and the recovery assertions come back from history. * test(evals): pin the managed defect without typing (no editor without a model) With no selectable model the composer renders no contenteditable editor, so the pinned block asserts what is really there: the persistent notice with Retry and no crash. * test(evals): pin only the persistent notice after Retry (the button reads Refreshing mid-flight) * feat(evals): make surface placement explicit, and own browser disposal resolveHost() reads one ambient env var, so a whole process got one host: everything local, or everything in one sandbox. A spec could not say 'app here, browser there' or 'two desktops in two sandboxes' — the topology was inherited from the environment instead of declared. - desktop({ host }) and the new chrome({ host }) take a placement. - localHost() / daytonaSandbox(id) name one, with REPO_ROOT wiring kept inside the package. daytonaSandbox() fails loudly from inside a sandbox, where the CLI indirection has nothing to target. - resolveHost() default behaviour is unchanged, so existing specs are untouched. chrome() also fixes a real leak: attachSurface only closes the CDP socket, so the hand-rolled spawnChrome + attachSurface in first-run-cloud-share left a browser process running after every run. Disposing now stops the client and then disposes the surface, which is what kills it.
* feat(markdown): render LaTeX math in chat with KaTeX OpenWork rendered Markdown but had no math engine, so $...$, $$...$$, \(...\) and \[...\] all showed up as raw LaTeX source in technical conversations (reported against v0.18.12). - Add a markdown-math marked extension: marked-katex-extension covers the standard dollar delimiters (same approach as upstream opencode web), plus local block/inline extensions for the \(...\) and \[...\] pairs models commonly emit, which markdown would otherwise swallow as escapes. - Register it on both the sync and Shiki-highlighted parsers so formulas do not flicker when a message with a code block upgrades. - Allow <semantics>/<annotation> through DOMPurify; they are not in its default MathML allowlist, so the accessible MathML branch (and copy-as-TeX) was being stripped. - Wrap every math renderer so malformed LaTeX degrades to visible source instead of throwing out of the React render and blanking the message. - Skip .katex when applying find-in-conversation highlights. - Import katex CSS and keep formulas at the surrounding text size. Tests: apps/app/tests/markdown-math.test.ts covers all four delimiters, MathML survival through sanitization, graceful failure, currency amounts, code spans, both parsers and both presentations. * test(evals): stage the math proof seed so each frame shows new content * test(evals): match inline-stage formula count * Potential fix for pull request finding 'CodeQL / Improper code sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…box-aware ports, profile pruning (#3388) * fix(evals): ask the host for its workspace root instead of assuming the driver's A spec passing process.cwd() as a workspace path is only correct when the driver and the app share a filesystem. Driving a sandbox from a laptop, the app was asked to open a directory that exists only on the laptop — observed as onboarding hanging 120s on 'Power your first task' with no error, which reads like a broken app rather than a bad path. Host.workspaceRoot states it (repoRoot locally, /workspace on Daytona) and DesktopHandle re-exposes it, so app-smoke asks the app's own host. * fix(evals): allocate Daytona surface ports that are free INSIDE the sandbox The local host finds a free port by binding one; the Daytona host cannot, because the port must be free on another machine. It allocated from a local counter instead, so the OpenCode sidecar — which picks its own port at boot — was observed holding 9825, the CDP primary. Electron's debugger never bound and the preview URL timed out after 180s with nothing naming the cause. Ask the sandbox which ports are listening and skip those. * fix(evals): app-smoke opens a fresh small folder, not the repo root Routing app-smoke through Host.workspaceRoot made it open the whole monorepo (/workspace, node_modules included). The engine scan then blocked the renderer past 240s and the spec failed reading location.hash — a regression versus the old process.cwd() (which happened to be the smaller /workspace/evals). workspaceRoot stays the right primitive for specs that genuinely need the repo (skills-local reads .opencode/skills); a workspace path just needs to be small and valid on the app's host. * fix(evals): prune stale surface profiles, not just their processes Disposal removes a profile on the happy path, but every killed or failed run leaks one at ~50MB. Twenty-seven filled a 10GB sandbox to 99%, and the symptom was not 'disk full' — it was the renderer failing to answer Runtime.evaluate for 240s, indistinguishable from a broken app. Cost four debug cycles to find. clearStaleSurfaces already pkills by rootDir and so already assumes exclusive ownership; deleting the directories it just orphaned is the matching half. * test(evals): two members each connect their own account to one org connector and call its tools Two SEPARATE desktops, own profiles, signed in as different members. A single desktop clearing localStorage only simulates per-member isolation; two make it real — the load-bearing assertion is that member B still needs their own sign-in after member A connected. The connector is the authority on use: mock MCP now records tool ARGUMENTS and a sha256 fingerprint of the caller's bearer token, so the spec asserts on calls actually served and on DISTINCT credentials rather than the app's own 'Connected' text. Placement: both desktops use the ambient host; moving either to its own sandbox is desktop({ host: daytonaSandbox(id) }) once mode B is green. * fix(evals): email verification is best effort; the sign-in that follows is the test ensureMemberSession threw when mark-verified failed, even though the member had just been created successfully and could sign in. On the eval sandbox the helper uses -uroot with no password while den's MariaDB root has one, so bootstrapping a second member aborted the whole spec at 2s. Warn and continue; if the subsequent sign-in fails, report both causes. * test(evals): wait for the connections surface to settle, and refresh the way the proven spec does * test(evals): name what was on screen when a connection card never renders * test(evals): steer back to settings when the app opens a new session mid-poll The card poll was watching the session surface: the app opens a freshly created session on its own and navigates away from settings. Re-navigate each iteration instead of assuming the route stays put. * test(evals): short, failure-tolerant probes while two desktops contend Two desktops in one sandbox make the renderer freeze in bursts; a bare 20s Runtime.evaluate turns one freeze into a failed spec. Probe briefly and retry, which is what the polling loop is for. * test(evals): gate the connector spec on the blank-connections defect The org connections surface renders empty for a signed-in org member on current dev — correct route, empty body text. org-connection-lifecycle (untouched by this branch) independently regressed the same way today after the sandbox pulled merged dev, which includes #3375 across the connect flow. Everything before that surface is verified. Opt-in via OPENWORK_EVAL_CONNECTOR_SPEC=1 so a product defect cannot masquerade as a nightly failure; remove the gate when the surface renders. * test(evals): match the canonical extensions route the app rewrites to The app canonicalises /workspace/<id>/settings/extensions/connections to /workspace/<id>/extensions/connections. Checking for the pre-rewrite form made the steer-back re-navigate every iteration, fighting the rewrite so the surface never settled — which presented as a blank page and which I wrongly attributed to a product regression. A probe found no render error and 31 buttons on the canonical route. * fix(evals): name the expression when a CDP evaluation times out The rejection fires from a timer, so the caller's stack is already gone: 'CDP call Runtime.evaluate timed out after 20000ms' cannot say which of a spec's dozens of evaluations blocked. Include a truncated expression. * fix(evals): retry navigation through renderer freeze bursts go() set the hash with a bare 20s evaluate. Under contention (two desktops on one host) a freeze burst killed the whole journey at its last phase. Setting a hash is idempotent, so retry it. * test(evals): frame the detail panel once it has painted, not once its text is in the DOM waitForText proves the DOM; a frame proves pixels. The detail panel paints slightly after its text lands, so the vision check intermittently caught a blank panel — it passed one run and failed the next. Wait for a laid-out node, scroll it into view, allow a paint. * test(evals): match rendered text when revealing, not raw textContent waitForText reads innerText, which is render-aware: a badge styled with CSS text-transform reads NEEDS YOUR SIGN-IN on screen while textContent holds 'Needs your sign-in'. The reveal could never agree with the wait. * fix(evals): give navigation a budget that survives a starved renderer * test(evals): settle canonical connection lifecycle frames * test(evals): avoid duplicate lifecycle proof frame * test(evals): prove two explicit Daytona desktop placements * test(evals): deduplicate disconnected lifecycle frame * fix(evals): make an externally-managed mock connector usable across runs publicUrl was a dead seam: waitForHealth read child?.exitCode on the null child and declared the (never-spawned) server exited, and the request log of a long-lived mock satisfied atLeast with a PREVIOUS run's tool calls, returning before this run's calls arrived. Guard the health check to only fail on a spawned child that exited, carry the mock's timestamp on each tool call, and let callers scope toolCalls with sinceIso. * test(evals): place each connector member's desktop on its own Daytona sandbox Two desktops plus two engines starve renderers on one 9GB sandbox — that ceiling, not any product defect, is what kept the tool-call phase red, so the header's retracted defect story goes too. OPENWORK_EVAL_DAYTONA_SANDBOX_A/_B now place each member via desktop({ host: daytonaSandbox(id) }), the mock publishes through OPENWORK_EVAL_CONNECTOR_MOCK_PUBLIC_URL so Den, both desktop browsers, and the driver can all reach it, and this run's tool calls are scoped with sinceIso. Green twice end to end (133.0s, 135.2s): both members OAuth-connect their own account, call mock_echo from the composer, and the connector witnesses two distinct bearer fingerprints. * fix(evals): parse the mock's bearer header in linear time CodeQL: js/polynomial-redos — ^Bearer\s+(.+)$ backtracks polynomially on 'Bearer' followed by header spam. One shared linear parse for the auth check and the fingerprint.
* ci: add Warden security clearance for PR diffs Warden runs a single custom skill (diff-security-review) on every PR to dev and answers one question: does this diff introduce a new security issue? A workflow_run gatekeeper grants a 'security clearance' review when the diff is clean, satisfying the required-review gate in the branch ruleset. Humans still review and merge. - Clearance identity is a dedicated GitHub App that never pushes, so require_last_push_approval holds by construction. - The gatekeeper executes from the default branch and refuses to clear PRs touching .github/, warden.toml, .warden/, or agent skill dirs. - dismiss_stale_reviews_on_push revokes clearance on every new push; Warden re-runs and re-clears if still clean. - No-ops until WARDEN_MODEL, WARDEN_ANTHROPIC_API_KEY, WARDEN_APP_ID, and WARDEN_PRIVATE_KEY secrets are configured. * ci: close clearance gaps in Warden workflows - Drop ready_for_review trigger: warden.toml cannot match it, so the run would report findings_count=0 without analyzing and wrongly grant clearance on draft-to-ready PRs. - Skip fork PRs in warden.yml (no secrets on forks; clearance already fork-blocked downstream) instead of failing red. - Add timeout-minutes to both jobs and serialize clearance runs per branch (queued, not cancelled, so revokes are never killed). * ci: switch Warden to openai/gpt-5.6-luna Agent and auxiliary lanes both use gpt-5.6-luna: auxiliary verification gates which findings survive, so it stays on the same strong model to avoid false clearances. Secret becomes WARDEN_OPENAI_API_KEY. Validated locally against the real CLI: injection+XSS diff -> 1 high (same-root-cause merge), XSS-only diff -> 1 high, clean diff -> 0 findings. 3-7s per run. * ci: trigger Warden analyze with live secrets * ci: pin Warden workflow actions to commit SHAs Addresses Warden's own finding on this PR: the token-minting action ran from a mutable tag while holding the app private key. All actions in both workflows are now SHA-pinned. * ci: grant contents:write so Warden can resolve fixed threads Resolving review threads via GraphQL requires contents:write on the GITHUB_TOKEN; without it the report step fails after fix evaluation and stale findings block merge under thread-resolution rules. * ci: set least-privilege permissions on electron build workflow Fixes both open CodeQL actions/missing-workflow-permissions alerts; neither job writes to the repo or GitHub API.
Seats and model access both cost $10 per user and were rendered as two near-identical cards, so people bought one believing they had bought the other — and seat checkout runs in Stripe setup mode ($0 today) while model access charges the whole team immediately, which the page never said. Billing now opens on a single statement that names both line items with their own status and price and sums them into a total, so no charge is unaccounted for. Each card states who is billed, and every action is paired with a sentence describing when to use it instead of standing alone as a bare button. Deployments without Stripe keys no longer quote prices or offer a checkout the operator cannot complete. Adds DenUsageMeter, DenActionRow/DenActionList, and DenLineItemRow/DenMarkTile, and extends DenNotice with neutral and warning tones. Co-authored-by: Cursor <cursoragent@cursor.com>
… connector room (#3416) * feat(evals): one-command provisioning for the two-member connector spec Getting org-connector-two-members green took ~50 minutes of hand orchestration across four machines, and two of its failures were pure operator traps. This turns the hands into code: - test-server-on-daytona.sh --seed: the demo-org seed now runs inside start-daytona-server.sh with the exact env the Den API got. Seeded by hand with env that diverged (a missing DEN_ORG_MODE), the seed refused email signup while the API allowed it. - @openwork/hosts provisioning primitives, one job each: provisionDenSandbox (server script + seeded-org sign-in proof), provisionDesktopSandbox (create-or-reuse, checkout, install, disk gate, display, xdg-open browser-hop proof, Vite prewarm), startMockOnSandbox (public-issuer mock, health + issuer assert), deleteSandboxes, renderConnectorSpecEnv/parseConnectorSpecEnv. Every remote command travels as ONE bash -lc argument: daytona exec joins args with spaces, so anything else silently drops commands. - scripts/provision-org-connector-two-members.ts: composes the primitives for that one spec (1 Den + mock + 2 desktops), writes the spec's exact env contract, and --delete removes only what it created. Six new unit tests pin the traps (single-argument law, disk gate, issuer mismatch, --yes fallback, env round-trip); 101 pass. * fix(evals): give the Den sign-in proof a settling window and a voice First live run: a freshly-booted stack answered public sign-in with bare 403s for its first ~minute, then recovered — and the proof both gave up at 30s and discarded the response body that would have named the reason. Widen to 120s and keep the body in the failure message. * refactor(evals): delete the speculative --yes deletion branch The installed CLI rejects --yes outright, so the try-flag-then-fall-back dance made every deletion pay a guaranteed failure first. Always answer the confirmation prompt on stdin; a promptless CLI ignores it. * fix(evals): send Origin on the sign-in proof like every real client The captured body named it: a fresh stack answers origin-less POSTs with 403 MISSING_OR_NULL_ORIGIN for its first minutes. Browsers and the app always send Origin; only this synthetic probe did not. * fix(evals): stop the browser-hop gate from pkilling its own shell The cleanup pattern is a substring of the bash -lc command line carrying it, so pkill TERMed the gate's shell (exit 143) before the verdict echoed. Verdict first, bracketed patterns so self-match is impossible. * fix(evals): browser-hop gate must not run chromium as a child of the exec session Chromium starting inside a pipe-stdin daytona exec session TERMs the session (exit 143 ~2.5s in; identical script survives under a TTY). Both halves now fully detach the way the mock and Vite gates do, and childless polls read the proof back. Verified through the node exec path on a live sandbox, which also exercises the reuse path end to end. * fix(evals): trustworthy checkout pinning and a first-boot gate for desktop rooms Two failures from live bisecting, both now impossible: - The checkout gate detached to FETCH_HEAD, which a refused raw-sha fetch leaves stale — the sandbox silently ran the wrong tree and the 'resolved' log line lied. It now checks out the requested ref (remote-tracking first), falls back to a full fetch, and ASSERTS the resolved sha when a sha was asked for. The assertion caught two mispins within the hour. - A sandbox's first Electron boot pays sidecar prepare, the openwork-server tsc build, and the engine cold start. Paid inside a spec, that bill starved the tool-call phase past its 240s window while every UI assertion still passed — reading exactly like a product regression (an A/B/A repin exonerated the suspect app commit). The provisioner now boots a throwaway profile once, waits for CDP, and tears it down: the room is only ready when the app has booted. * fix(evals): parse the env header by line scan, not backtracking regex CodeQL js/polynomial-redos: '.*; ref=' backtracks polynomially on a header padded with repeated '; ref=a'. Scan for the prefix and take the last marker instead, with render and parse sharing the literals so they cannot drift. The adversarial input now parses in ~1ms.
…lisher, pr-photo-roll skill (S1) (#3417)
…org-wide publishing admin-only (#3411)
…able without a marketplace (skill-sharing P1) (#3410) * feat(den-api): grant-native capability index — skills usable without a marketplace (skill-sharing P1) * test(specs): prove grant-native skill access over the real MCP wire (skill-sharing P1) * test(evals): self-diagnosing member bootstrap for single-org Dens
… the parity check (#3421)
…ll share offer, duplicate-plugin 409 (skill-sharing P2) (#3422)
…dge (P3 PR1) (#3424) * feat(den): team access visibility — aggregate edge endpoint + team detail screen (skill-sharing P3 PR1) * fix(evals): navigate via CDP Page.navigate parameter instead of constructed code (CodeQL js/bad-code-sanitization)
* feat(den-web): make the plugin catalogue readable The marketplace and plugin screens identified every entry with a colour slab hashed from its id, so a catalogue of real products looked like a swatch palette, and the two-column card grid meant counts never lined up. Replace the rail with the entry's own logo (marketplaces have one and it was going unused) or a monogram, and render each surface as a single divided list where counts share one column. Lead the marketplace directory with the catalogues that actually contain something instead of whatever was created last. Invert the cloud-readiness badge: it announced "Cloud ready" — true of every row, so it said nothing — while suppressing the two states that need a human. It is now silent when a plugin is ready and speaks only when action is needed. Co-authored-by: Cursor <cursoragent@cursor.com> * test(specs): marketplace-catalog-legibility as a photo-roll spec, retiring the legacy flow The catalogue redesign proof moves from evals/flows (runner flow + voiceover) to evals/specs/marketplace-catalog-legibility.slow.test.ts: the same six claims — per-entry identity, one aligned count column, stocked-first order, greyed zeros, readiness that only speaks when a plugin needs action, and a plugin detail that opens with the identity the person clicked — now driven through a real Chrome against den-web and captured as a photo roll with vision-validated frames. signInInBrowser also accepts landing on /dashboard as a sign-in outcome: plain web sign-in redirects there, while the desktop handoff keeps showing 'signed in' / the openwork:// code. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…role choice, blast-radius archive (skill-sharing P3 PR2) (#3425)
…provenance debt retirement (skill-sharing P3 PR3) (#3429)
…mber signs in to each, agent picks by name (#3427) * feat(den-api): record the connected Google account, offer the account chooser, and stop deep-linking to u/0 Foundation for multiple Google Workspace connectors per organization. No user-visible multi-account behavior yet; every change here is additive and backward compatible. - external_mcp_connection gains kind + native_provider_key so a row can represent a native OAuth connector instead of an external MCP server (migration 0050, additive, no constraints and no backfill) - the native OAuth callback now resolves the account's email from the id_token, falling back to the provider's userinfo endpoint, and persists it as externalAccountId. Resolution is strictly best-effort: a missing claim, a failed fetch, or a junk response still completes the connect exactly as before - Google's authorize prompt becomes "consent select_account" so a member who is already signed in is actually offered the account chooser - Gmail draft and thread links use authuser=<email> instead of the hardcoded mail/u/0, which resolves to whichever Google account happens to be first in the browser. Falls back to the exact previous u/0 output when the email is unknown The redirect_uri, every route path, and all generated capability names are unchanged. * test(evals): drop the stub multi-account flow, keep the approved script `pnpm fraimz --all` runs every file in evals/flows on the nightly lane, so a scaffolded flow whose frames are still `ctx.assert(false)` turns nightly red until the feature lands. New app-driving proof belongs in evals/specs as a photo-roll spec (see org-connector-two-members.slow.test.ts and the pr-photo-roll skill), which the nightly spec lane runs separately and which publishes evidence to the PR. The voiceover script stays as the approved narration for that spec. * test(evals): red spec for two Google domains, plus the primitive surfaces it needs Writes the proof before the feature. The spec enacts the approved narration in evals/voiceovers/google-workspace-multi-account.md and encodes the definition of done, including the halves that are easy to skip: - the mailbox is the witness: the supplier draft must appear in the Labs mailbox AND the Robotics mailbox must be empty - the two connectors' drafts must not share a credential token - the user-facing request must not contain either connector id, so "the agent picked the right connector" cannot pass by being told the answer - the Gmail link must carry authuser= and must not be /mail/u/0/ - a single-domain organization must still see exactly one card and one button Six primitives it needs do not exist yet. They are declared here as typed stubs that throw a message naming the primitive and how to build it, so the spec compiles and fails for the right reason instead of failing to compile: startMockGoogle, createNativeConnector, readSessionToolCalls, provisionOrg. openConnectionsSurface/waitForConnectionCard/revealText are promoted out of org-connector-two-members rather than stubbed, since working copies existed. Gated behind OPENWORK_EVAL_GOOGLE_MULTI_ACCOUNT_SPEC so an unconfigured run skips cleanly; it is .slow, so it never enters the PR lane. * test(evals): implement startMockGoogle — a Google fixture that can tell two accounts apart The first of the six primitives the multi-account spec needs. Written as a focused mock beside scripts/mock-oauth-mcp-server.mjs rather than an extension of it: six existing eval flows depend on that script's single-account behaviour, and the safest way to not regress them is to leave it byte-unchanged. The fixture provides what the old one structurally could not: - an id_token carrying the authorizing account's email, plus /userinfo, so Den can record WHICH account connected - a real chooser page when prompt contains select_account, with chooseAccount() resolving only once the callback has actually been served, never on a timer - per-account mailboxes, every draft attributed to the credential that created it, so a spec can assert the draft landed in one inbox and not the other draftsFor() returns promptly with [] for an empty mailbox instead of blocking for the full timeout, because the negative half of the isolation assertion is the half that matters and a primitive that makes it slow is a primitive that makes it get dropped. * feat(den-api): native provider connectors — many Google domains per organization An organization can now register more than one Google Workspace connector. A connector is an external_mcp_connection row with kind='native_provider': its OAuth client lives in org_oauth_client and each member's tokens live in connected_account, both keyed by the connector row id. The existing (org_membership_id, provider_id) unique index then makes per-connector isolation automatic rather than something to enforce. Backward compatibility is by alias, not migration. An organization that already configured Google keeps provider_id='google-workspace' literally in both tables and keeps appearing with id "google-workspace". Nothing is rewritten, because rewriting provider_id would look like an OAuth client rotation and revoke every member's tokens. The redirect URI stays keyed by provider, not by connector, so customers do not have to touch the Google Cloud Console entry they already pasted. The connector id travels in the signed OAuth state instead. Native rows are fenced out of every MCP handshake path — transport, catalog, tool calls, connect/start discovery, the external OAuth callback binding, and plugin MCP requirement binding — since a Google connector has no MCP server behind it and would otherwise fail deep inside a handshake. Capability namespacing is deliberately not here: un-namespaced Google capability names still resolve to the organization's default connector. test(evals): implement provisionOrg, and make the local Den reproducible provisionOrg creates a real isolated organization with a known shape so a spec can assert that a single-domain company still sees exactly one card. evals/scripts/dev-den.ts captures the setup that previously took manual archaeology: db:push rather than db:migrate (the chain cannot build a database from scratch), an isolated port and database, and the trusted origins that otherwise fail as a 403 at sign-in looking like a product bug. * test(evals): implement createNativeConnector, and ask for a feature that exists The spec was requesting a "gmail" scope feature that the provider registry does not define, and the route correctly rejected it. The first fix attempt added "gmail" to NATIVE_OAUTH_PROVIDERS as an alias for the compose scope, which is product code bending to fit a test; reverted. The spec now asks for gmailDraft, which is the real feature name for creating drafts. createNativeConnector posts the connector and its OAuth client through the route that landed with the server work, and deleteConnection is confirmed to work on native rows so the spec's onTestFinished cleanup is real. * test(evals): frame the pixels, and claim what the product actually shows Two fixes found by running the spec against the real app rather than reasoning about it. The DOM assertions passed while the frames failed: the connection card was in the document but not on screen, so the vision model saw a truncated workspace name instead. Every screenshot now reveals its subject first, which is what revealText exists for — waitForText proves the DOM contains it, a frame proves pixels. The first frame also claimed Acme Robotics was "set up", which conflated the admin having registered the connector with the member having connected their own account. At that point in the story Jordan has not connected, and the app correctly offers her a sign-in. The claim now says what a person would see. Frames 1 and 2 pass: two separate Google connections visible side by side. * test(evals): assert what the desktop actually offers, and drop an incidental layout claim Two more corrections found by running against the real app. The list surface offers each connection a "Sign in" next action and opens a detail panel to authorize; "Connect your account" (mcp.org_connection_connect_action) only exists in that panel. The third frame was asserting the panel's wording against the list, so it counted zero buttons. It now asserts the two cards plus the sign-in affordance, and the later connect flow already goes through the detail panel correctly. The second frame claimed the two connections were "side by side". They are stacked vertically, and the vision model was right to say so — it passed once and then failed on the same pixels, because the claim asserted incidental layout rather than the fact being proven. Claims should describe what a human would verify, or they are flaky by construction. * feat(desktop,den-api): say which service a connector fronts, and which account is signed in The member card for a native connector was anonymous: an admin names it "Acme Robotics", and nothing on screen said Google Workspace. The vision check caught this as a real product gap — it passed or failed depending on whether the model credited the G favicon, which is what a human reviewer would waver on too. The usable-connections entries now carry nativeProviderKey (both the toConnectionResponse mapper and buildNativeProviderEntry — native rows are served by the latter, which is why fixing only the former changed nothing), plus the caller's connected account email. The desktop prefixes the card description with the service name and, once connected, appends "Signed in as <email>." — keeping the existing sentence intact as a prefix because other specs wait for it verbatim. Also: frames now assert prompt CONTAINS select_account (the product correctly sends "consent select_account"), the detail-panel frame replaces a pixel-duplicate list frame, and openConnectionsSurface steers back in a loop — the app opens a freshly created session on its own, and a single navigation followed by a 60s wait parks the spec on /session forever. Its failure now reports what was actually on screen. * feat(desktop): badge each connected connector with its signed-in account "Connected" alone cannot tell two Google sign-ins apart, and the approved demo narration promises the list shows the real email behind each connector. The connected badge now reads "Connected — jordan@acmelabs.test" when the account identity is known. The spec's both-emails frame closes the Labs panel first and waits for both addresses on the list before framing it. * chore(den-db): regenerate connector-kind migration as 0051 after dev landed its own 0050 * fix(den-api): enforce member access grants on native connector OAuth (connect/start, callback, status, disconnect, default resolution) Warden NPN-7MR: an ungranted member could begin OAuth on a native connector and back un-namespaced Google capabilities with it. Every member-facing resolution of a connector row now goes through the grant-aware listing; the legacy org_oauth_client-only alias keeps its implicitly org-wide behavior unchanged.
…search placement, card/chip fidelity; spec now pins design (#3430)
…, named connectors (#3432) The Google tile stops being a singleton: it always opens the dialog, the dialog takes a name ("Acme Labs"), and submit creates a native connector row via POST /v1/mcp-connections instead of overwriting the org's single legacy client. Legacy rows still edit the old client. Disconnect routing on web and desktop now decides by nativeProviderKey on the payload rather than id === "google-workspace", so connector rows disconnect through the oauth-providers path; Your Connections shows the signed-in email for Google rows like it already did for Microsoft.
…ers, @openwork/testkit, fraimz:publish (#3431)
…needs-admin-setup, MCP transport facet in filters (#3434)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why
Issue
Scope
Out of scope
Testing
Ran
...Result
CI status
Manual verification
Evidence
N/A (docs-only)Risk
Rollback